home *** CD-ROM | disk | FTP | other *** search
/ Celestin Apprentice 5 / Apprentice-Release5.iso / Source Code / C++ / Applications / PICSee Dust 1.01 / Tertiary Source / Select Multiple Files.c < prev    next >
Encoding:
Text File  |  1995-11-18  |  32.3 KB  |  908 lines  |  [TEXT/CWIE]

  1. ///// Select Multiple Files Demo 1.0 • By Eddy J. Gurney / ©1994 The Eccentricity Group
  2. ////
  3. /// See "Select Multiple Files.c" for more information.
  4. //
  5.  
  6. // I don't like #define's...
  7.  
  8. /* Character codes */
  9. enum {
  10.     homeKey = 0x01,
  11.     enterKey = 0x03,
  12.     endKey,
  13.     helpKey,
  14.     deleteKey = 0x08,
  15.     tabKey,
  16.     pageUpKey = 0x0b,
  17.     pageDownKey,
  18.     returnKey,
  19.     anyFunctionKey = 0x10,
  20.     escapeKey = 0x1b,
  21.     leftArrow = 0x1c,
  22.     rightArrow = 0x1d,
  23.     upArrow = 0x1e,
  24.     downArrow = 0x1f
  25. };
  26.  
  27. /* Other useful constants */
  28. enum {
  29.     kControlInactive = 255,
  30.     kControlActive = 0,
  31.     kListInset = -1,
  32.     kScrollBarWidth = 16,
  33.     kScrollBarAdjust = (kScrollBarWidth - 1)
  34. };
  35.  
  36. enum {
  37.     rErrorAlert = 500,
  38.     rTotalDialog = 501
  39. };
  40.  
  41. enum {
  42.     rSFMultipleFileDialog = 509,
  43.     dSFMultipleLine1 = 10,
  44.     dSFMultipleSelectedFilesList,
  45.     dSFMultipleOpenButton,
  46.     dSFMultipleAddAllButton,
  47.     dSFMultipleRemoveButton,
  48.     dSFMultiplePromptStaticText,
  49.     dSFMultipleLine2
  50. };
  51.  
  52. enum {
  53.     rChangingButtonStrings = 509,
  54.     kOpenFileButton = 1,
  55.     kOpenListButton
  56. };
  57.  
  58. // ---------------------------------------------------------------------------
  59.  
  60. ////////
  61. /////// Select Multiple Files Demo 0.99 • By Eddy J. Gurney / ©1994 The Eccentricity Group
  62. //////
  63. ///// The code below demonstrates how to present a dialog box that allows the user to
  64. //// select multiple files, ala THINK C's "Add Files..." option. This version is better, however,
  65. /// since the user can easily select a single file without clicking two buttons, and because
  66. // the user can "Tab" between the two lists.
  67. //
  68. // This is NOT a trivial task, as you'll see from the amount of code necessary to support
  69. // this feature. I worked on this every night for over a week trying to get things to work
  70. // the way I wanted them to for an application I'm writing. After I got it working, I saw
  71. // several requests in comp.sys.mac.programmer for this type of functionality, so I put
  72. // together this demo app. My application did NOT require all the features I demonstrate
  73. // here -- my application uses files in the order the user clicked them, not alphabetical
  74. // order like shown here (alpha order is better for most apps, though) and my app allows
  75. // the user to select a particular file as many times as they'd like, not once like shown
  76. // here (I didn't need the "file filter" code, but provided one in the demo since others will).
  77. // Since removing features is easier than adding them, I added those features in hope that
  78. // they would prove useful to someone.
  79. //
  80. // That said, let me say that there are probably still bugs in this code, and that this is only
  81. // meant to be a "starting point" for you! Neither Hewlett-Packard nor myself are responsible
  82. // for anything that happens from the use of this code. That is, #include <std/disclaimer.h>
  83. //
  84. // Very special thanks to Ari Halberstadt, for his "WinterShell", which got me started
  85. // down the right path for this endeavour, and to Scott Knaster / Keith Rollin, authors of
  86. // "Macintosh Programming Secrets" for lots of insight when I was learning to program
  87. // the Mac. Extra special thanks to Lloyd Lim of Lim Unlimited for allowing me to use his
  88. // wonderful method of drawing gray outlines. The original source for this material was
  89. // his "Default" CDEF, version 2.4.
  90. //
  91. // If you use this code, or any code derived from it, in your application, you should include
  92. // my name in your "About" box and/or documentation. I'd like to hear about it via e-mail
  93. // as well. And if you want to give me a registered copy, that would be fine with me, too!
  94. // Other than that, there are no restrictions on the use or distribution of this code, as
  95. // long as this notice, my name and copyright remains intact when redistributing it.
  96. //
  97. // Oh yeah, if you do find any bugs, you improve the code, find this useful, or have any
  98. // suggestions, want to give me money, etc., please let me know!
  99. //
  100. // Eddy J. Gurney <egurney@vcd.hp.com>
  101. // v0.97 April 26, 1994 (My initial working version of this demo. Never released.)
  102. // v0.98 November 30, 1994 (Cleaned up code for release on the Apprentice CD-ROM.)
  103. //
  104. // To do:
  105. //   * Add support for keyboard in "selected files" list
  106. //   * Clean up Balloon Help
  107. //
  108. // PS: I write code in Geneva 9 with tabs every 4 spaces, so you may have to adjust
  109. // your editor to view this like I do. :-)
  110.  
  111. #include "Select Multiple Files.h"
  112. #include "ArrayListLib.h"
  113. #include <GestaltEqu.h>    /* For Gestalt constants */
  114. #include <Packages.h>    /* For IUMagString */
  115. #include <Palettes.h>    /* For GetGray() */
  116. #include <LowMem.h>
  117.  
  118. /* Local types */
  119. typedef struct {
  120.     StandardFileReply    *theReply;    /* the reply record */
  121.     ListHandle            selectedList;    /* handle to the select file list */
  122.     ArrayListHandle    files;        /* list of selected files */
  123.     short            numTypes;
  124.     SFTypeList        typeList;
  125.     Boolean            active;        /* true if our file selection list is active */
  126.     Boolean            findNextFile;
  127.     short            nextCharacter;
  128.     short            extraEvent;
  129. } SFMultipleFileData;
  130.  
  131. /* For FrameItemInGray() */
  132. typedef struct {
  133.     PenState        penState;
  134.     RGBColor        backColor;
  135.     RGBColor        frameColor;
  136.     Rect            bounds;
  137.     Boolean        inactive;
  138.     Boolean        roundFrame;
  139.     Boolean        erasingOld;
  140. } GrayFrameData, *GrayFrameDataPtr;
  141.  
  142. /* Function prototypes */
  143. static pascal Boolean SFMultipleFileModalFilter(DialogPtr, EventRecord *, short *, void *);
  144. static pascal short SFMultipleFileDialogHook(short, DialogPtr, void *);
  145. static void DeselectListItems(ListHandle);
  146. static void AddFileToList(DialogPtr, SFMultipleFileData *, FSSpec *, Boolean);
  147. static pascal short ListMgrCompString(Ptr, Ptr, short, short);
  148. static Point InsertInOrder(StringPtr, ListHandle);
  149. static Point InsertByAppending(StringPtr stringToAdd, ListHandle theList);
  150. static Boolean IsValidFileType(short, SFTypeList, OSType);
  151. static Boolean FileAlreadyAdded(short, long, StringPtr, SFMultipleFileData *);
  152. static pascal void SFMultipleFileActivate(DialogPtr, short, Boolean, void *);
  153. static pascal Boolean SFMultipleFileFilter(ParamBlockRec *, void *);
  154. static void ToolBoxInit(void);
  155. static void FlashDialogItem(DialogPtr, short);
  156. static void GetDItemRect(DialogPtr, short, Rect *);
  157. static void HiliteDialogItem(DialogPtr, short, short);
  158. static void MySetDialogDefaultItem(DialogPtr, short, Boolean);
  159. static pascal void FrameItemInGray(WindowPtr, short);
  160. static pascal void GrayFrameDrawingProc(short, short, GDHandle, GrayFrameDataPtr);
  161. static void SetGrayFrameDialogItem(DialogPtr, short);
  162. static void SetDItemTitle(DialogPtr, short, StringPtr);
  163.  
  164. #define ErrorHandler(argument)
  165.  
  166. // ---------------------------------------------------------------------------
  167.  
  168. /* Global variables */
  169. long                gQuickDrawVersion;
  170.  
  171. void SelectPICTFilesToMerge(short *numFiles, FSSpec **fileList) {
  172.     StandardFileReply    theSFReply;
  173.     Point                where = { -1, -1 };
  174.     short            activeInputItemsList[] = { 2, sfItemFileListUser, dSFMultipleSelectedFilesList };
  175.     SFMultipleFileData    fileData;
  176.     ModalFilterYDUPP modalFilterProc;
  177.     DlgHookYDUPP dialogHookYDProc;
  178.     ActivateYDUPP activateProc;
  179.     FileFilterYDUPP fileFilterProc;
  180.     
  181.     /* Initialize the multiple file data structure before we use it */
  182.     fileData.theReply = &theSFReply;
  183.     fileData.selectedList = nil;
  184.     fileData.files = ArrayListBegin(sizeof(FSSpec)); /* Start our list of FSSpec's */
  185.     fileData.numTypes = 1;
  186.     fileData.typeList[0] = 'PICT';
  187.     fileData.active = false;
  188.     fileData.findNextFile = false;
  189.     fileData.extraEvent = 0;
  190.  
  191.     /* Present the multiple file dialog */
  192.     fileFilterProc = NewFileFilterYDProc(SFMultipleFileFilter);
  193.     modalFilterProc = NewModalFilterYDProc(SFMultipleFileModalFilter);
  194.     dialogHookYDProc = NewDlgHookYDProc(SFMultipleFileDialogHook);
  195.     activateProc = NewActivateYDProc(SFMultipleFileActivate);
  196.  
  197.     CustomGetFile(fileFilterProc,
  198.         fileData.numTypes,
  199.         fileData.typeList,
  200.         &theSFReply, 
  201.         rSFMultipleFileDialog,
  202.         where,
  203.         dialogHookYDProc,
  204.         modalFilterProc,
  205.         activeInputItemsList,
  206.         activateProc,
  207.         &fileData);
  208.     
  209.     DisposeRoutineDescriptor(fileFilterProc);
  210.     DisposeRoutineDescriptor(modalFilterProc);
  211.     DisposeRoutineDescriptor(dialogHookYDProc);
  212.     DisposeRoutineDescriptor(activateProc);
  213.  
  214.     if (theSFReply.sfGood) { /* The user clicked the "Open All" ("Open File") button */
  215.         FSSpec        fileSpec;
  216.         short        index;
  217.  
  218.         /* Handle the case where the user clicked on the "Open File" button without adding any */
  219.         if (ArrayListCount(fileData.files) == 0) {
  220.             ArrayListInsert(fileData.files, 0);
  221.             ArrayListSet(fileData.files, 0, &theSFReply.sfFile);
  222.         }
  223.  
  224.         /* Now do something interesting with the selected files.
  225.             This shows how to access the files the user has selected. */
  226.  
  227.         *numFiles = ArrayListCount(fileData.files);
  228.         *fileList = (FSSpec*)NewPtr(sizeof(FSSpec) * (*numFiles));
  229.         for (index = 0; index < *numFiles; index++) {
  230.             ArrayListGet(fileData.files, index, &fileSpec);
  231.             BlockMove(&fileSpec, &(*fileList)[index], sizeof(FSSpec));
  232.         }
  233.     }
  234.  
  235.     ArrayListEnd(fileData.files);
  236. }
  237.  
  238. /******************************************************************/
  239.  
  240. /* Handle the dialog events */
  241. static pascal Boolean SFMultipleFileModalFilter(DialogPtr theDialog, EventRecord *theEvent, short *itemHit, void *myDataPtr)
  242. {
  243.     GrafPtr            savePort;
  244.     short            iType;
  245.     Handle            iHandle;
  246.     Rect                iRect;
  247.     SFMultipleFileData    *fileData = myDataPtr;
  248.     Boolean            result = false;
  249.  
  250.     GetPort(&savePort);
  251.  
  252.     switch (theEvent->what) {
  253.         case updateEvt: {
  254.             PenState    savePen;
  255.  
  256.             if (/* IsAppWindow((WindowPtr)theEvent->message) */ 0) {
  257.                 /* IsAppWindow is a function you provide that returns true if the passed WindowPtr
  258.                    belongs to your application. If it does, then you need to call your application's update
  259.                    function to redraw any windows which have Balloon Help holes in them, etc. */
  260.                 /* ProcessUpdateEvent((WindowPtr)theEvent->message); */
  261.              } else {
  262.                 /* It wasn't one of our application windows, so it must be our dialog */
  263.                 SetPort(theDialog);
  264.                 GetPenState(&savePen);
  265.                 PenNormal();
  266.                 LUpdate(theDialog->visRgn, fileData->selectedList);
  267.                 GetDItem(theDialog, dSFMultipleSelectedFilesList, &iType, &iHandle, &iRect);
  268.                 FrameRect(&iRect);
  269.                 if (fileData->active) {
  270.                     InsetRect(&iRect, -3, -3);
  271.                     PenSize(2, 2);
  272.                     FrameRect(&iRect);
  273.                 }
  274.                 SetPenState(&savePen);
  275.             }
  276.             break;
  277.         }
  278.         case activateEvt:
  279.             if (/* IsAppWindow((WindowPtr)theEvent->message) */ 0) {
  280.                 /* IsAppWindow is a function you provide that returns true if the passed WindowPtr
  281.                    belongs to your application. If it does, then you need to call your application's activate
  282.                    function to activate/deactivate the specified window, etc. */
  283.                 /* ProcessActivateEvent((WindowPtr)theEvent->message, theEvent->modifiers & activeFlag); */
  284.             } else {
  285.                 /* It wasn't one of our application windows, so it must be our dialog */
  286.                 SetPort(theDialog);
  287.                 SFMultipleFileActivate(theDialog, dSFMultipleSelectedFilesList, 
  288.                     fileData->active && (theEvent->modifiers & activeFlag) != 0, fileData);
  289.                 LActivate((theEvent->modifiers & activeFlag) != 0, fileData->selectedList);
  290.             }
  291.             break;
  292.         case mouseDown:
  293.             if (GetWRefCon(theDialog) == sfMainDialogRefCon) {
  294.                 Point        where = theEvent->where;
  295.  
  296.                 SetPort(theDialog);                
  297.                 GlobalToLocal(&where);
  298.  
  299.                 GetDItemRect(theDialog, dSFMultipleSelectedFilesList, &iRect);
  300.                 if (PtInRect(where, &iRect)) {
  301.                     /* A double click in the "selected files" list is the same as clicking "Remove"... */
  302.                     if (LClick(where, theEvent->modifiers, fileData->selectedList)) {
  303.                         *itemHit = dSFMultipleRemoveButton;
  304.                         FlashDialogItem(theDialog, *itemHit);
  305.                         result = true;
  306.                     }
  307.                 }
  308.             }
  309.             break;
  310.         case keyDown:
  311.         case autoKey:
  312.             if (GetWRefCon(theDialog) == sfMainDialogRefCon) {
  313.                 char        key = theEvent->message;
  314.                 
  315.                 SetPort(theDialog);
  316.                 if ((theEvent->modifiers & cmdKey) != 0) {
  317.                     switch (key) {
  318.                         /* Handle any dialog Command-key equivalents for the dialog here, 
  319.                            like Cmd-A for Add All, or something? */ 
  320.                     }
  321.                 } else if (fileData->active && key != tabKey) {
  322.                     /* Ooops. This isn't quite done yet! You need to handle the up/down keys
  323.                     here, add the ability for the user to type the first few characters of an
  324.                     entry and jump to it, etc. See Ari's "WinterShell" if you need help! */
  325.                 }
  326.             }
  327.             break;
  328.     }
  329.     
  330.     SetPort(savePort);
  331.     return result;
  332. }
  333.  
  334. static pascal short SFMultipleFileDialogHook(short item, DialogPtr theDialog, void *myDataPtr)
  335. {
  336.     GrafPtr            savePort;
  337.     SFMultipleFileData    *fileData = myDataPtr;
  338.     Cell                cell;                    /* for accessing cells in list */
  339.     short            index;                /* index for inserting/removing from list */
  340.     
  341.     GetPort(&savePort);
  342.     SetPort(theDialog);
  343.     
  344.     if (GetWRefCon(theDialog) == sfMainDialogRefCon) {
  345.         Boolean    isFile = !fileData->theReply->sfIsVolume && !fileData->theReply->sfIsFolder && *fileData->theReply->sfFile.name;
  346.  
  347.         /* Select the next file in the list for the user OR adjust the dialog buttons. */
  348.         if (fileData->selectedList) {
  349.             /* Ok, this is a real wizzy feature. When you add a file, it is removed from the "top" list
  350.               of files, and the hilite for that list just "disappears". That means the user has to manually
  351.               select the next file to add. (If you really want to see what I'm talking about, just comment
  352.               out the whole if statement below and click the "Add" button.) Since this code hilites the last file
  353.               added in the "bottom" list, we can get its name... and simulate the user typing in each character
  354.               of the name! This means the hilite moves to the file (or directory) AFTER the file the user just
  355.               added. Cool, huh? It IS a little sluggish right now, probably because of the two List Manager
  356.               calls. It would be faster to keep a copy of the filename we just added in the fileData structure,
  357.               but this was a quick-and-dirty last minute hack, so I'll leave performance optimizations as an
  358.               exercise for the programmer. :-) */
  359.             if (fileData->findNextFile && !fileData->active) {
  360.                 Str63    fileName;
  361.                 short    length = sizeof(fileName);
  362.                 
  363.                 cell.h = cell.v = 0;
  364.                 if (LGetSelect(true, &cell, fileData->selectedList)) {
  365.                     LGetCell(fileName, &length, cell, fileData->selectedList);
  366.                     if (fileData->nextCharacter < length) {
  367.                         item = sfHookCharOffset + *(fileName + fileData->nextCharacter);
  368.                         fileData->nextCharacter++;
  369.                         return item;
  370.                     }
  371.                     fileData->findNextFile = false;
  372.                 }
  373.             }
  374.  
  375.             /* See the comments at the end of the "Remove" button code for why we have this */
  376.             if (fileData->extraEvent) {
  377.                 item = fileData->extraEvent;
  378.                 fileData->extraEvent = 0;
  379.                 return item;
  380.             }
  381.             
  382.             cell.h = cell.v = 0;
  383.             /* If a file in the "selected" list is selected, enable the Remove button */
  384.             HiliteDialogItem(theDialog, dSFMultipleRemoveButton, LGetSelect(true, &cell, fileData->selectedList) ? kControlActive : kControlInactive);
  385.             /* If the focus is in the list of files, enable the Add All button */
  386.             HiliteDialogItem(theDialog, dSFMultipleAddAllButton, !fileData->active ? kControlActive : kControlInactive);
  387.             /* Enable our "Open" button if a file is selected, or if files have been added */
  388.             if ((ArrayListCount(fileData->files) > 0) || (!fileData->active && isFile))
  389.                 HiliteDialogItem(theDialog, dSFMultipleOpenButton, kControlActive);
  390.             else
  391.                 HiliteDialogItem(theDialog, dSFMultipleOpenButton, kControlInactive); 
  392.             /* Change the default button depending on where the input focus is */
  393.             if (fileData->active) {
  394.                 if (ArrayListCount(fileData->files) == 0) {
  395.                     MySetDialogDefaultItem(theDialog, sfItemCancelButton, false);
  396.                 } else {
  397.                     MySetDialogDefaultItem(theDialog, dSFMultipleOpenButton, false);
  398.                 }
  399.             } else {
  400.                 if (*fileData->theReply->sfFile.name) {
  401.                     MySetDialogDefaultItem(theDialog, sfItemOpenButton, true);
  402.                 } else {
  403.                     MySetDialogDefaultItem(theDialog, sfItemCancelButton, false);
  404.                 }
  405.             }            
  406.         }
  407.         switch (item) {
  408.             case sfHookFirstCall:
  409.             {
  410.                 Rect        iRect;
  411.                 Rect        rDataBounds = { 0, 0, 0, 1 };    /* Data bounds of list */
  412.             
  413.                 /* Create the list */
  414.                 cell.h = cell.v = 0;
  415.                 GetDItemRect(theDialog, dSFMultipleSelectedFilesList, &iRect);
  416.                 InsetRect(&iRect, -kListInset, -kListInset);
  417.                 iRect.right -= kScrollBarAdjust;
  418.                 fileData->selectedList = LNew(&iRect, &rDataBounds, cell, 0, theDialog, true, false, false, true);
  419.                 if (fileData->selectedList == nil)
  420.                     ErrorHandler("\pCouldn't create the selected file list!!");
  421.                 SetGrayFrameDialogItem(theDialog, dSFMultipleLine1);
  422.                 SetGrayFrameDialogItem(theDialog, dSFMultipleLine2);
  423.                 MySetDialogDefaultItem(theDialog, sfItemOpenButton, false);
  424.                 break;
  425.             }
  426.             case sfHookLastCall:
  427.                 /* Dispose of the list */
  428.                 LDispose(fileData->selectedList);
  429.                 fileData->selectedList = nil;
  430.                 break;
  431.             case sfItemOpenButton:
  432.                 /* Add file to list */
  433.                 if (*fileData->theReply->sfFile.name && !fileData->active && isFile) {
  434.                     AddFileToList(theDialog, fileData, &fileData->theReply->sfFile, true);
  435.                     fileData->findNextFile = true;
  436.                     fileData->nextCharacter = 0;
  437.                     item = sfHookRebuildList;
  438.                 }
  439.                 break;
  440.             case dSFMultipleOpenButton:
  441.                 if (isFile || (ArrayListCount(fileData->files) > 0))
  442.                     /* Fake the system into thinking we selected the file by clicking on its "Open" button */
  443.                     item = sfItemOpenButton;
  444.                 else
  445.                     item = 0;
  446.                 break;
  447.             case dSFMultipleRemoveButton:
  448.                 cell.h = cell.v = 0;
  449.                 /* There can be more than one item selected in the "bottom" list, so remove all of 'em */
  450.                 while (LGetSelect(true, &cell, fileData->selectedList)) {
  451.                     index = cell.v;
  452.                     LDelRow(1, index, fileData->selectedList);
  453.                     ArrayListDelete(fileData->files, index);
  454.                     cell.h = cell.v = 0;
  455.                 }
  456.                 /* If there is only 1 item in the list, change the "Open List" button back to "Open File" */ 
  457.                 if (ArrayListCount(fileData->files) == 0) {
  458.                     Str31    openString;
  459.                     
  460.                     GetIndString(openString, rChangingButtonStrings, kOpenFileButton);
  461.                     SetDItemTitle(theDialog, dSFMultipleOpenButton, openString);
  462.                 }
  463.                 /* We want to be "cool"... so clicking on the remove button will select the "selected files"
  464.                    (bottom) list, unless we're removing the last item, in which case we automatically switch
  465.                    the input focus to the "file" list. We can only return one "item" (rebuildList in this case) so
  466.                    we handle it "special"... */
  467.                 if (ArrayListCount(fileData->files) == 0) {
  468.                     /* If there are no items left in the list, switch the focus back to the file list */
  469.                     fileData->extraEvent = sfHookSetActiveOffset + sfItemFileListUser;
  470.                 } else {
  471.                     fileData->extraEvent = sfHookSetActiveOffset + dSFMultipleSelectedFilesList;
  472.                 }
  473.                 item = sfHookRebuildList;
  474.                 break;
  475.             case dSFMultipleAddAllButton:
  476.             {
  477.                 FSSpec        fsSpec;
  478.                 CInfoPBRec    pb;
  479.                 Str63        name;
  480.                 Boolean        alreadyDeselected = false;
  481.                 
  482.                 LDoDraw(false, fileData->selectedList);    /* Turn off list updating while we add files */
  483.                 pb.hFileInfo.ioVRefNum = -LMGetSFSaveDisk();    /* The current directory SF is in */
  484.                 pb.hFileInfo.ioNamePtr = name;    /* Where the name of each entry will be placed */
  485.                 index = 1;
  486.                 do {
  487.                     pb.hFileInfo.ioDirID = LMGetCurDirStore();    /* You need to set this each time through the loop! */
  488.                     pb.hFileInfo.ioFDirIndex = index++;
  489.                     PBGetCatInfo(&pb, false);    /* Get file info */
  490.                     /* If successful GetCatInfo, the entry is a file, the file type is OK, and it hasn't been added, add it */
  491.                     if (pb.hFileInfo.ioResult == noErr && (pb.hFileInfo.ioFlAttrib & 0x10) == 0 &&
  492.                        IsValidFileType(fileData->numTypes, fileData->typeList, pb.hFileInfo.ioFlFndrInfo.fdType) &&
  493.                        !FileAlreadyAdded(pb.hFileInfo.ioVRefNum, pb.hFileInfo.ioFlParID, pb.hFileInfo.ioNamePtr, fileData)) {
  494.                            if (!alreadyDeselected) {
  495.                             DeselectListItems(fileData->selectedList);
  496.                             alreadyDeselected = true;
  497.                         }
  498.                         /* Create an FSSpec, since that is what we store for each added entry */
  499.                         fsSpec.vRefNum = pb.hFileInfo.ioVRefNum;
  500.                         fsSpec.parID = pb.hFileInfo.ioFlParID;
  501.                         BlockMove(pb.hFileInfo.ioNamePtr, fsSpec.name, *pb.hFileInfo.ioNamePtr + 1);
  502.                         AddFileToList(theDialog, fileData, &fsSpec, false);
  503.                     }
  504.                 } while (pb.hFileInfo.ioResult == noErr);
  505.                 LDoDraw(true, fileData->selectedList);    /* Turn drawing back on */
  506.                 LUpdate(theDialog->visRgn, fileData->selectedList);    /* And update the list */
  507.                 item = sfHookRebuildList;
  508.             }
  509.             break;
  510.         }
  511.     }
  512.     
  513.     SetPort(savePort);
  514.     return item;
  515. }
  516.  
  517. /* We deselect any items already selected before adding a new file */
  518. static void DeselectListItems(ListHandle theList)
  519. {
  520.     Cell        theCell = {0, 0};
  521.     
  522.     while (theCell.v < (*theList)->dataBounds.bottom) {
  523.         LSetSelect(false, theCell, theList);
  524.         theCell.v++;
  525.     }
  526. }
  527.  
  528. /* Add the information passed to the specified list */
  529. void AddFileToList(DialogPtr theDialog, SFMultipleFileData *fileData, FSSpec *theReply, Boolean doDeselect)
  530. {
  531.     Point        cell;
  532.     short    index;
  533.     
  534.     /* Insert name into the list */
  535.     cell = InsertByAppending(theReply->name, fileData->selectedList);
  536.     index = cell.v;
  537.     /* Insert into list of files, at index corresponding to item in display list, which makes it easy to
  538.        remove the item when the user hits Remove button */
  539.     ArrayListInsert(fileData->files, index);
  540.     ArrayListSet(fileData->files, index, theReply);
  541.     if (doDeselect)
  542.         /* Deselect any currently selected items... */
  543.         DeselectListItems(fileData->selectedList);
  544.     /* ...select the item we just added... */
  545.     LSetSelect(true, cell, fileData->selectedList);
  546.     /* ...and scroll it into view. */
  547.     LAutoScroll(fileData->selectedList);
  548.     /* If this is the first item we're adding to the list, change the "Open File" button to "Open List" */
  549.     if (index == 0) {
  550.         Str31    openString;
  551.         
  552.         GetIndString(openString, rChangingButtonStrings, kOpenListButton);
  553.         SetDItemTitle(theDialog, dSFMultipleOpenButton, openString);
  554.     }
  555. }
  556.  
  557. /* Compare string 'aPtr' and 'bPtr', and return 0 if the 'aPtr' string is
  558.    greater than the 'bPtr' string, otherwise return 1. */
  559. pascal short ListMgrCompString(Ptr aPtr, Ptr bPtr, short aLen, short bLen)
  560. {
  561.     if (IUMagString(aPtr, bPtr, aLen, bLen) == 1)
  562.         return 0;
  563.     else
  564.         return 1;
  565. }
  566.  
  567. /* Add the string 'stringToAdd' to the list 'theList', in alphabetical order
  568.    using the search procedure ListMgrCompString() */
  569. Point InsertInOrder(StringPtr stringToAdd, ListHandle theList)
  570. {
  571.     Point        cell = {0, 0};
  572.  
  573.     (void)LSearch((Ptr)stringToAdd + 1, (short)*stringToAdd, 
  574.         (ListSearchUPP)&ListMgrCompString, &cell, theList);
  575.     (void)LAddRow(1, cell.v, theList);
  576.  
  577.     LSetCell(stringToAdd + 1, (short)*stringToAdd, cell, theList);
  578.     
  579.     return cell;
  580. }
  581.  
  582. /* Add the string to the list by putting it at the end of the list.
  583.     Added by HTD 11/1/95 */
  584. Point InsertByAppending(StringPtr stringToAdd, ListHandle theList) {
  585.     Point cell = {0,0};
  586.  
  587.     cell.v = (**theList).dataBounds.bottom+1;
  588.     cell.v = LAddRow(1, cell.v, theList);
  589.  
  590.     LSetCell(stringToAdd + 1, (short)*stringToAdd, cell, theList);
  591.     
  592.     return cell;
  593. } // END InsertByAppending
  594.  
  595. /* Return 'true' if the specified file type is in the type list */
  596. Boolean IsValidFileType(short numTypes, SFTypeList typeList, OSType fileType)
  597. {
  598.     Boolean    isValid = false;
  599.     short    index;
  600.     
  601.     for (index = 0; !isValid && index < numTypes; index++)
  602.         isValid = fileType == typeList[index];
  603.         
  604.     return isValid;
  605. }
  606.  
  607. /* If the specified file parameters match a file in the list, return true since it has already been added */
  608. Boolean FileAlreadyAdded(short ioVRefNum, long ioFlParID, StringPtr namePtr, SFMultipleFileData *fileData)
  609. {
  610.     FSSpec            **files;
  611.     short            nfiles;
  612.     SignedByte        state;
  613.     Boolean            found;
  614.     short            i;
  615.  
  616.     found = false;
  617.     files = ArrayListGetHandle(fileData->files);
  618.     nfiles = ArrayListCount(fileData->files);
  619.     state = HGetState((Handle)files);
  620.     HLock((Handle)files);
  621.  
  622.     for (i = 0; !found && i < nfiles; i++) {
  623.         /* Check and see if a file is already in the list and return true if it is... */
  624.         found = (ioVRefNum == (*files)[i].vRefNum &&
  625.                 ioFlParID == (*files)[i].parID &&
  626.                 (EqualString(namePtr, (*files)[i].name, true, true) == true));
  627.     }
  628.  
  629.     HSetState((Handle)files, state);
  630.  
  631.     return found;
  632. }
  633.  
  634. /* Change the hiliting of the list to indicate that it is active and receiving keyboard input */
  635. static pascal void SFMultipleFileActivate(DialogPtr theDialog, short item, Boolean activating, void *myDataPtr)
  636. {
  637.     GrafPtr            savePort;
  638.     PenState            savePen;
  639.     SFMultipleFileData    *fileData = myDataPtr;
  640.     
  641.     GetPort(&savePort);
  642.     SetPort(theDialog);
  643.     GetPenState(&savePen);
  644.     
  645.     activating = (activating != 0);    /* uses 0xFF for true, so convert to Boolean */
  646.  
  647.     if (item == dSFMultipleSelectedFilesList) {
  648.         Rect        theRect;
  649.         
  650.         fileData->active = activating;
  651.         if (!activating)
  652.             PenPat(&qd.white);
  653.         PenSize(2, 2);
  654.         GetDItemRect(theDialog, item, &theRect);
  655.         InsetRect(&theRect, -3, -3);
  656.         FrameRect(&theRect);
  657.     }
  658.  
  659.     SetPenState(&savePen);
  660.     SetPort(savePort);
  661. }
  662.  
  663. /* Filter files that have already been "added" */
  664. static pascal Boolean SFMultipleFileFilter(ParamBlockRec *pb, void *myDataPtr)
  665. {
  666.     CInfoPBRec    *cat = (CInfoPBRec *)pb;        /* this isn't documented but seems to work */
  667.     Boolean        hide;
  668.  
  669.     if ((pb->fileParam.ioFlAttrib & 0x10) == 0) {    /* param block describes a file */
  670.         hide = FileAlreadyAdded(cat->hFileInfo.ioVRefNum, cat->hFileInfo.ioFlParID, 
  671.                             cat->hFileInfo.ioNamePtr, myDataPtr);
  672.     } else {
  673.         hide = false;
  674.     }
  675.  
  676.     return hide;
  677. }
  678.  
  679. /* Quickly flash a button in the dialog */
  680. void FlashDialogItem(DialogPtr theDialog, short theItem)
  681. {
  682.     short    iType;
  683.     Handle    iHandle;
  684.     Rect        iRect;
  685.     long        ignored;
  686.  
  687.     GetDItem(theDialog, theItem, &iType, &iHandle, &iRect);
  688.     HiliteControl((ControlHandle)iHandle, 1);
  689.     Delay(8, &ignored);
  690.     HiliteControl((ControlHandle)iHandle, 0);
  691. }
  692.  
  693. /* Return the enclosing rectangle of the specified dialog item, in coordinates
  694.    local to the specified dialog. */
  695. void GetDItemRect(DialogPtr theDialog, short theItem, Rect *iRect)
  696. {
  697.     Handle    iHandle;
  698.     short    iType;
  699.     
  700.     GetDItem(theDialog, theItem, &iType, &iHandle, iRect);
  701. }
  702.  
  703. /* Make a control active/inactive, or highlight/dim the specified part */
  704. void HiliteDialogItem(DialogPtr theDialog, short theItem, short code)
  705. {
  706.     short    iType; 
  707.     Handle    iHandle; 
  708.     Rect        iRect;
  709.  
  710.     GetDItem(theDialog, theItem, &iType, &iHandle, &iRect);
  711.     HiliteControl((ControlHandle)iHandle, code);
  712. }
  713.  
  714. /****************************************************
  715.     SetDialogItemTitle
  716.  
  717.     Change the text associated with a particular dialog item. This is a little
  718.     tricky since there are two ways to change the text. If you are dealing
  719.     with an EditText or StatText (static text) item, you must call SetIText.
  720.     If you are dealing with a dialog item that is backed up by a Control
  721.     Manager control (like a simple button, radio button, or checkbox), you
  722.     must call SetCTitle.
  723.  
  724.     We determine what kind of dialog item we are dealing with by calling
  725.     GetDItem. Returned in the “kind” parameter is a number that identifies
  726.     what sort of item we are handling. First, we strip off the upper bit,
  727.     which identifies the item as being enabled or disabled. Once that bit is
  728.     removed, we can examine the kind of the item and act accordingly.
  729.  
  730.     Notice the special handling we give to controls when we call SetCTitle.
  731.     This is to take care of “excessive flashing” as Online Companion puts it.
  732.     When you call SetCTitle, the control manager first calls HideControl to
  733.     remove the control with its old text from the screen. It then changes the
  734.     control’s title in the ControlRecord, and reshows the control by calling
  735.     ShowControl. At this point, the control is properly shown on the screen
  736.     with its correct, new title.
  737.  
  738.     However, there’s a little time bomb lurking in the works. When HideControl
  739.     was called, the Control Manager called InvalRect on the area the control
  740.     occupied. Even though ShowControl was later called on the same area and
  741.     everything is drawn correctly, that rectangle is still marked as invalid
  742.     and is incorporated into the update region for the dialog. The event loop
  743.     at the heart of ModalDialog will then get an update event for that area
  744.     and redraw the button _again_! This can cause the button to flicker and
  745.     flash more than we would like. We already know that that area is
  746.     adequately drawn, so we tell the Event Manager to hoof it by validating it
  747.     with a call to ValidRect.
  748. *****************************************************/
  749. void SetDItemTitle(DialogPtr theDialog, short theItem, StringPtr newTitle)
  750. {
  751.     short    iType;
  752.     Handle    iHandle;
  753.     Rect        iRect;
  754.  
  755.     GetDItem(theDialog, theItem, &iType, &iHandle, &iRect);
  756.     iType &= ~itemDisable;    /* Strip off the enable/disable bit */
  757.     if ((iType == statText) || (iType == editText)) {
  758.         SetIText(iHandle, newTitle);
  759.     } else {
  760.         SetCTitle((ControlHandle)iHandle, newTitle);
  761.         SetPort(theDialog);
  762.         ValidRect(&iRect);
  763.     }
  764. }
  765.  
  766. /****************************************************
  767.     SetGrayFrameDialogItem    Set the specified dialog item to have a special
  768.                         drawing procedure which will simply outline the
  769.                         item's rectangle in "gray". Dividing lines can be
  770.                         easily defined this way by setting the height or
  771.                         width of the user item to 1.
  772. *****************************************************/
  773. void SetGrayFrameDialogItem(DialogPtr theDialog, short theItem)
  774. {
  775.     Rect            iRect;
  776.     Handle        iHandle;
  777.     short        iType;
  778.  
  779.     GetDialogItem(theDialog, theItem, &iType, &iHandle, &iRect);
  780.     SetDialogItem(theDialog, theItem, iType, (Handle)FrameItemInGray, &iRect);
  781. }
  782.  
  783. void MySetDialogDefaultItem(DialogPtr theDialog, short theItem, Boolean forceActive)
  784. {
  785.     if (((DialogPeek)theDialog)->aDefItem != theItem) {
  786.         GrafPtr        savePort;
  787.         GrayFrameData    frameData;
  788.     
  789.         GetPort(&savePort);
  790.         SetPort(theDialog);
  791.  
  792.         /* Erase the old default button */        
  793.         GetPenState(&frameData.penState);
  794.         GetDItemRect(theDialog, ((DialogPeek)theDialog)->aDefItem, &frameData.bounds);
  795.         InsetRect(&frameData.bounds, -4, -4);
  796.         frameData.roundFrame = true;
  797.         frameData.inactive = false;
  798.         frameData.erasingOld = true;
  799.         GrayFrameDrawingProc(0, 0, nil, &frameData);
  800.  
  801.         ((DialogPeek)theDialog)->aDefItem = theItem;
  802.         if (forceActive)
  803.             HiliteDialogItem(theDialog, theItem, kControlActive);
  804.         FrameItemInGray(theDialog, theItem);
  805.         
  806.         SetPort(savePort);
  807.     }
  808. }        
  809.  
  810. /*******************************************************
  811.     FrameItemInGray() and GrayFrameDrawingProc() are adapted routines based
  812.     primarily on Lloyd Lim’s wonderful method of drawing things in "gray" correctly
  813.     in his "Default" CDEF. He gave me permission to use them here, and for that I am
  814.     indebted! This is the best method I’ve seen for drawing things across multiple
  815.     monitors with different bit depths, etc. Very slick!
  816. *******************************************************/
  817. pascal void FrameItemInGray(WindowPtr theDialog, short theItem)
  818. {
  819.     short        iType;
  820.     ControlHandle    theControl;
  821.     GrayFrameData    frameData;
  822.     RGBColor        foreColor;
  823.     AuxCtlHandle    theAuxCtl;
  824.     DeviceLoopDrawingUPP deviceLoopProc;
  825.  
  826.     GetPenState(&frameData.penState);
  827.  
  828.     GetDItem(theDialog, theItem, &iType, (Handle *)&theControl, &frameData.bounds);
  829.     if ((iType & ~itemDisable) == (ctrlItem | btnCtrl)) {
  830.         frameData.roundFrame = true;
  831.         InsetRect(&frameData.bounds, -4, -4);
  832.         frameData.inactive = ((*theControl)->contrlHilite == kControlInactive);
  833.     } else {
  834.         frameData.roundFrame = false;
  835.         frameData.inactive = true;    /* Since we want to draw userItem outlines in gray */
  836.     }
  837.     frameData.erasingOld = false;
  838.  
  839.     if (gQuickDrawVersion >= gestalt8BitQD) {
  840.         GetForeColor(&foreColor);
  841.         if (frameData.roundFrame) {
  842.             (void)GetAuxCtl(theControl, &theAuxCtl);
  843.             frameData.frameColor = (*(*theAuxCtl)->acCTable)->ctTable[cFrameColor].rgb;
  844.         } else {
  845.             frameData.frameColor = foreColor;
  846.         }
  847.     }
  848.     if (gQuickDrawVersion >= gestalt32BitQD13 && theDialog->portBits.rowBytes & 0xC000) {
  849.         GetBackColor(&frameData.backColor);
  850.         deviceLoopProc = NewDeviceLoopDrawingProc(GrayFrameDrawingProc);
  851.         DeviceLoop(theDialog->visRgn, deviceLoopProc,
  852.             (long)&frameData, 0);
  853.         DisposeRoutineDescriptor(deviceLoopProc);
  854.     } else {
  855.         GrayFrameDrawingProc((gQuickDrawVersion >= gestalt8BitQD), 0, nil, &frameData);
  856.     }
  857.     if (gQuickDrawVersion >= gestalt8BitQD) {
  858.         RGBForeColor(&foreColor);
  859.     }
  860. }
  861.  
  862. static pascal void GrayFrameDrawingProc(short depth, short deviceFlags, GDHandle targetDevice,
  863.      GrayFrameDataPtr frameData)
  864. {
  865.     RGBColor        outlineColor;
  866.     short        radius;
  867.     
  868.     PenNormal();
  869.  
  870.     if (frameData->erasingOld) {
  871.         PenPat(&qd.white);
  872.     } else {
  873.         if (depth)
  874.             outlineColor = frameData->frameColor;
  875.         if (frameData->inactive)
  876.             if (targetDevice == nil || GetGray(targetDevice, &frameData->backColor, &outlineColor) == false)
  877.                 PenPat(&qd.gray);
  878.         if (depth)
  879.             RGBForeColor(&outlineColor);
  880.     }
  881.     if (frameData->roundFrame) {
  882.         PenSize(3, 3);
  883.         if ((radius = ((frameData->bounds.bottom - frameData->bounds.top) >> 1)) < 16)
  884.             radius = 16;
  885.         FrameRoundRect(&frameData->bounds, radius, radius);
  886.     } else {
  887.         FrameRect(&frameData->bounds);
  888.     }
  889.  
  890.     SetPenState(&frameData->penState);
  891. }
  892.  
  893. /* This is just a sample error handler... that is totally lame! You really need to look
  894.    up the error messages in a string resource and stuff, not use string constants like
  895.    I've shown here! (Hey, this wasn't a demo app showing how to handle errors! :-) */
  896. /*
  897. void ErrorHandler(StringPtr errorString)
  898. {
  899.     short    itemHit;
  900.  
  901.     SetCursor(&qd.arrow);
  902.     SetDAFont(systemFont);
  903.     ParamText(errorString, nil, nil, nil);
  904.     itemHit = StopAlert(rErrorAlert, nil);
  905.     ExitToShell();
  906. }
  907. */
  908.